#!pip install plotly
#!pip install folium

Schienennetz vor dem Kollaps - so titeln Marie Blöcher, Nils Naber und Isabel Schneider vom NDR. Die Deutsche Bahn habe zwar ehrgeizige Ziele, allerdings ist Jahrelang zu wenig Geld ins Netz geflossen.
Rund 60 Milliarden Euro müssten laut DB ausgegeben werden, um alle Probleme im Netz zu beheben, die sich über die vergangenen Jahre angesammelt haben. Der Zustand von Strecken und Gleisen wurde über viele Jahre vernachlässigt, sagt Bahnexperte Christian Böttger.
Die Bahn steht aktuell in keinem guten Licht. Zu viele Verspätungen, Zugausfälle und marode Infrastruktur.
Doch wie steht es wirklich um den Zustand der Bahn?
In dieser Analyse wird auf Daten der Deutschen Bahn zugegriffen, um dieser Frage auf den Grund zu gehen.
Die Bahn stellt über den API Marketplace eine Fülle an Daten offen und kostenfrei zur Verfügung.

Im Rahmen dieses Projekts betrachen wir folgende Daten:
Über folgende APIs werden die Daten im JSON-Format abgerufen, in pandas dataframes umgewandelt und gespeichert.
url_parking = 'https://apis.deutschebahn.com/db-api-marketplace/apis/parking-information/db-bahnpark/v2/'
url_ris_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/ris-stations/v1/'
url_rw_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/api.railway-stations.org/photoStationById/'
url_facility_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/fasta/v2/stations/'
url_parking_facilities = 'https://apis.deutschebahn.com/db-api-marketplace/apis/parking-information/db-bahnpark/v2/parking-facilities'
Häufig könne nicht alle Daten auf einmal abgerufen werden, daher müssen mehrere Aufrufe gemacht werden. Anschlißend ist ein mapping der (json) DB-Datenstruktur auf einen (flaches) Data Frame nötig.
Da der Abruf der Daten einige Minuten dauert, ist dieser und die eigentliche Auswertung getrennt in zwei verschiedenen files.
Wir nutzen Dateien im Pickel-Format, um ganze Dataframes zwischen dem Scraping-Process und der Datenauswertung auszutauschen.
Für die Visualisierung wird Plotly und Folium genutzt.
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import pickle
import plotly.express as px
import plotly.graph_objects as go
import folium
from geopy.geocoders import Nominatim
Hier werden die Daten aus den Pickel-Files in Dataframes geladen. Der Vorteil dabei ist, dass die Struktur und die Datenformate dabei beibehalten werden.
Das Data Cleaning wird allerdings hier umgesetzt.
data_folder = 'data/'
def loadData(fileName):
with open(data_folder + fileName, 'rb') as pkl_file:
return pickle.load(pkl_file)
df_stations = loadData('stations.pkl')
df_stopplaces = loadData('stopplaces_new.pkl')
df_facilities = loadData('station_facilities.pkl')
Es kann zwar mehrere Bilder pro Bahnhof geben, dies wurde beim data scraping allerdings bereits berücksichtigt, sodass hier immer genau ein Bild pro Bahnhof vorhanden ist.
Die Bilder sind noch als Dictionary gespeichert, sodass dieser hier noch in ein Pandas DataFrame umgewandelt werden.
df_station_images = loadData('station_images.pkl')
df_images = pd.DataFrame.from_dict({k: v for k, v in df_station_images.items() if v}).T
df_images.columns = ['image']
df_images = df_images.reset_index()
Wir sehen, dass die verschiedenen Daten, die eigentlich zusammengehören sollten, von der Anzahl her nicht komplett zusammenpassen.
Es ist aber durchaus erklärlich, dass es mehr Einrichtungen und Haltestellen als tatsächliche Bahnhöfe bzw. Bahnhofsgebäude gibt.
Wie genau die Daten aussehen, wird im Folgenden geprüft.
print(f'Stations: {df_stations.shape}')
print(f'Station images: {df_images.shape}')
print(f'Facilities: {df_facilities.shape}')
print(f'Stopplaces: {df_stopplaces.shape}')
Stations: (5690, 16) Station images: (5627, 2) Facilities: (3550, 6) Stopplaces: (5727, 9)
Das Data Frame der Bahnhöfe enthält alle Haltestellen der Deutschen Bahn in Deutschland.
Jeder Bahnhof hat eine eindeutige id. Zusältzich wird beispielsweise der Name, die Adresse und Geo-Koordinaten mitgeliefert.
df_stations.head(3)
| id | name | metropolis | street | houseNumber | postalCode | city | state | country | stationCategory | owner | organisationalUnit | countryCode | latitude | longitude | timeZone | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen Hbf | {} | Bahnhofstr. | 2a | 52064 | Aachen | Nordrhein-Westfalen | DE | CATEGORY_2 | DB S&S | RB West | DE | 50.767800 | 6.091499 | Europe/Berlin |
| 1 | 1000 | Burkhardswalde-Maxen | {} | Gesundbrunnen | 60c | 01809 | Müglitztal-Burkhardswalde | Sachsen | DE | CATEGORY_7 | DB S&S | RB Südost | DE | 50.925146 | 13.838369 | Europe/Berlin |
| 2 | 1001 | Burkhardtsdorf | {} | Bahnhofstraße | NaN | 09235 | Burkhardtsdorf | Sachsen | DE | CATEGORY_6 | DB Regio-Netze | Erzgebirgsbahn (EGB) | DE | NaN | NaN | Europe/Berlin |
Nutzer können Bilder zu Bahnhöfen hochladen.
Das Data Frame enthält Links zu diesen Bildern.
Die Spalte index referenziert die Spalte id der Bahnhöfe.
df_images.head(3)
| index | image | |
|---|---|---|
| 0 | 1 | https://api.railway-stations.org/photos/de/1_1... |
| 1 | 1000 | https://api.railway-stations.org/photos/de/100... |
| 2 | 1001 | https://api.railway-stations.org/photos/de/100... |
Hier ein Beispiel vom Bahnhof in Aachen.

Die Einrichtungen sind beispielsweise Geräte wie Aufzüge auf Bahnhöfen. Das interessante hier ist, dass auch der Zustand mitgegeben wird.
df_facilities.head(3)
| id | description | operatorname | state | stateExplanation | type | |
|---|---|---|---|---|---|---|
| 0 | 1 | zu Gleis 1 | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1 | 1 | zu Gleis 2/3 | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2 | 1 | zu Gleis 6/7 | DB Station&Service | ACTIVE | available | ELEVATOR |
In Wahrheit gibt es hier genau zwei verschiedene Arten von Einrichtungen: Aufzüge und Rolltreppen.
df_facilities['type'].unique()
array(['ELEVATOR', 'ESCALATOR'], dtype=object)
Haltestellen enthalten viele Informationen der Bahnhöfe, zusätzlich aber auch die Transportmittel und welcher Verkehrsbund hier fährt.
df_stopplaces.head(3)
| id | name | availableTransports | transportAssociations | countryCode | state | timeZone | latitude | longitude | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen Hbf | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AAV, VRS] | DE | NW | Europe/Berlin | 50.767800 | 6.091499 |
| 1 | 1000 | Burkhardswalde-Maxen | [REGIONAL_TRAIN] | [VVO] | DE | SN | Europe/Berlin | 50.925146 | 13.838369 |
| 2 | 1001 | Burkhardtsdorf | [REGIONAL_TRAIN, BUS] | [VMS] | DE | SN | Europe/Berlin | 50.733196 | 12.932137 |
Als erstest prüfen wir, ob die Data Frames korrekte Datentypen haben und korrigieren sie entsprechend.
Die Spalte id soll immer vom Typ int sein, um sie später besser zusammenführen zu können.
df_stations['id'] = df_stations['id'].astype(int)
df_stations.dtypes
id int32 name object metropolis object street object houseNumber object postalCode object city object state object country object stationCategory object owner object organisationalUnit object countryCode object latitude float64 longitude float64 timeZone object dtype: object
df_stopplaces['id'] = df_stopplaces['id'].astype(int)
df_stopplaces.dtypes
id int32 name object availableTransports object transportAssociations object countryCode object state object timeZone object latitude float64 longitude float64 dtype: object
df_facilities['id'] = df_facilities['id'].astype(int)
df_facilities.dtypes
id int32 description object operatorname object state object stateExplanation object type object dtype: object
df_images['index'] = df_images['index'].astype(int)
df_images.dtypes
index int32 image object dtype: object
Als nächsten wird auf fehlende Werte geprüft, um zu schauen, ob hier etwas zu tun ist.
df_stations.isna().sum()
id 0 name 0 metropolis 0 street 8 houseNumber 893 postalCode 7 city 4 state 0 country 0 stationCategory 12 owner 0 organisationalUnit 0 countryCode 0 latitude 282 longitude 282 timeZone 0 dtype: int64
Das Hausnummernfeld fehlt sehr oft, da wir diese Information aber hier nicht brauchen, ist das kein Problem.
Die Angaben für Latitude und Longitude fehlen auch häufig, hier kann über die Adresse versucht werden, die Werte herauszufinden.
Da das Nachschauen der Werte einige Zeit in Anspruch nimmt, ist dieser Code auskommentiert.
Grundsätzlich wird aber anhand der Spalten postalCode, city, state und country mithilfe des Pakets aus der Vorlesung geopy versucht, die Geo-Koordinaten aufzulösen.
# geolocator = Nominatim(user_agent="my_app")
# filtered_rows = df_stations[df_stations['latitude'].isnull()]
# result = {}
# # Print the entire row for each entry with NaN latitude
# for index, row in filtered_rows.iterrows():
# try:
# address = f'{row["postalCode"]} {row["city"]} {row["state"]} {row["country"]}'
# result[row['id']] = geolocator.geocode(address)
# except:
# pass
# dict={}
# # extract lat/lon
# for index, entry in result.items():
# if entry:
# dict[index] = { 'latitude': entry[1][0], 'longitude': entry[1][1] }
# geocode_result = pd.DataFrame().from_dict(dict).T
# geocode_result['id'] = geocode_result.index
# geocode_result['id'] = geocode_result['id'].astype(int)
Die Ergebnisse landen wieder in einem Pickel-File
# output = open(data_folder + 'manual_geocode_results.pkl', 'wb')
# pickle.dump(geocode_result, output)
# output.close()
geocode_result = loadData('manual_geocode_results.pkl')
df_stations
| id | name | metropolis | street | houseNumber | postalCode | city | state | country | stationCategory | owner | organisationalUnit | countryCode | latitude | longitude | timeZone | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen Hbf | {} | Bahnhofstr. | 2a | 52064 | Aachen | Nordrhein-Westfalen | DE | CATEGORY_2 | DB S&S | RB West | DE | 50.767800 | 6.091499 | Europe/Berlin |
| 1 | 1000 | Burkhardswalde-Maxen | {} | Gesundbrunnen | 60c | 01809 | Müglitztal-Burkhardswalde | Sachsen | DE | CATEGORY_7 | DB S&S | RB Südost | DE | 50.925146 | 13.838369 | Europe/Berlin |
| 2 | 1001 | Burkhardtsdorf | {} | Bahnhofstraße | NaN | 09235 | Burkhardtsdorf | Sachsen | DE | CATEGORY_6 | DB Regio-Netze | Erzgebirgsbahn (EGB) | DE | NaN | NaN | Europe/Berlin |
| 3 | 1002 | Bürstadt | {} | Bahnhofsallee | 17 | 68642 | Bürstadt | Hessen | DE | CATEGORY_6 | DB S&S | RB Mitte | DE | 49.645769 | 8.458188 | Europe/Berlin |
| 4 | 1005 | Buschow | {} | Bahnhofstr. | 28 | 14715 | Märkisch Luch OT Buschow | Brandenburg | DE | CATEGORY_6 | DB S&S | RB Ost | DE | 52.592203 | 12.628996 | Europe/Berlin |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 5685 | 995 | Burgstädt | {} | Bahnhofstr. | 1 | 09217 | Burgstädt | Sachsen | DE | CATEGORY_6 | DB S&S | RB Südost | DE | 50.915817 | 12.812707 | Europe/Berlin |
| 5686 | 996 | Burgstall (Murr) | {} | Bahnhofstr. | 1 | 71576 | Burgstetten | Baden-Württemberg | DE | CATEGORY_6 | DB S&S | RB Südwest | DE | 48.928647 | 9.369932 | Europe/Berlin |
| 5687 | 997 | Steinfurt-Burgsteinfurt | {} | Bahnhofsplatz | 6 | 48565 | Steinfurt-Burgsteinfurt | Nordrhein-Westfalen | DE | CATEGORY_6 | DB S&S | RB West | DE | 52.147384 | 7.329340 | Europe/Berlin |
| 5688 | 998 | Burgthann | {} | Bahnhofstr. | 40 | 90559 | Burgthann | Bayern | DE | CATEGORY_5 | DB S&S | RB Süd | DE | 49.342474 | 11.309307 | Europe/Berlin |
| 5689 | 999 | Regensburg-Burgweinting | {} | Alfons-Goppel-Straße | NaN | 93055 | Regensburg | Bayern | DE | CATEGORY_6 | DB S&S | RB Süd | DE | 48.990725 | 12.146486 | Europe/Berlin |
5690 rows × 16 columns
Nun liegen die vorhanden Geo-Koordinaten im Data Frame df_stations und die neu ermittelten in geocode_result.
Um nun das Ergebnis aus beiden Tabellen zu bekommen, wird die Funktion combine_first genutzt.
df_stations = df_stations.set_index('id').combine_first(geocode_result.set_index('id')).reset_index()
Anstelle von 282 fehlenden Werten sind es jetzt nur noch 24!
Die restlichen werden aufgrund der geringen Anzahl ignoriert.
df_stations.isna().sum()
id 0 city 4 country 0 countryCode 0 houseNumber 893 latitude 24 longitude 24 metropolis 0 name 0 organisationalUnit 0 owner 0 postalCode 7 state 0 stationCategory 12 street 8 timeZone 0 dtype: int64
df_stopplaces.isna().sum()
id 0 name 0 availableTransports 0 transportAssociations 0 countryCode 0 state 11 timeZone 0 latitude 0 longitude 0 dtype: int64
Die Haltestellen scheinen eine bessere Datenqualität zu haben, hier gibt es keine Probleme, die betrachtet werden müssen.
df_facilities.isna().sum()
id 0 description 51 operatorname 0 state 0 stateExplanation 0 type 0 dtype: int64
Einige Beschreibungen sind leer, hier muss geprüft werden, ob es sich dabei evtl. immer um den gleichen Typ handelt. Das gute ist, dass werder type noch state jemals leer ist.
Jetzt können alle Daten zusammengebracht werden.
Die Facilities können dabei nicht gejoined werden, da ein Bahnhof in der Regel mehrere davon aufweist (1:n Beziehung)
Dazu werden zunächst alle doppelten Spalten entfernt und dann die Tabellen mithilfe zweier merge zusammengefügt.
Davor wird nochmal stichprobenartig geprüft, ob die IDs auch wirklich zusammenpassen.
df_stopplaces[df_stopplaces['name'] == 'Ahrensfelde']
| id | name | availableTransports | transportAssociations | countryCode | state | timeZone | latitude | longitude | |
|---|---|---|---|---|---|---|---|---|---|
| 1539 | 28 | Ahrensfelde | [REGIONAL_TRAIN] | [VBB] | DE | BE | Europe/Berlin | 52.571375 | 13.565154 |
df_stations[df_stations['name'] == 'Ahrensfelde']
| id | city | country | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | timeZone | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 23 | 28 | Berlin | DE | DE | NaN | 52.571375 | 13.565154 | {} | Ahrensfelde | RB Ost | DB S&S | 12689 | Berlin | CATEGORY_4 | Märkische Allee | Europe/Berlin |
df_stopplaces.drop(columns=['name', 'state', 'countryCode', 'latitude', 'longitude','timeZone'], inplace=True)
df = pd.merge(df_stations, df_stopplaces, on='id', how='left')
df = pd.merge(left=df, right=df_images, left_on=['id'], right_on=['index'], how='left')
df.drop(columns=['timeZone','index','country'], inplace=True)
df.isna().sum()
id 0 city 4 countryCode 0 houseNumber 903 latitude 24 longitude 24 metropolis 0 name 0 organisationalUnit 0 owner 0 postalCode 7 state 0 stationCategory 12 street 8 availableTransports 18 transportAssociations 18 image 64 dtype: int64
Es fehlen nun noch einzelne Werte, aber mit dieser Datengrundlage kann gut gearbeitet werden.
Schauen wir uns an, was wir an Daten haben und welche Fragen sich daraus ergeben:
organisationalUnit mit dem owner korrelieren, vielleicht auch mit transportAssociations.transportAssociations überprüfen.stationCategory stehen.df.head(1)
| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen | DE | 2a | 50.7678 | 6.091499 | {} | Aachen Hbf | RB West | DB S&S | 52064 | Nordrhein-Westfalen | CATEGORY_2 | Bahnhofstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AAV, VRS] | https://api.railway-stations.org/photos/de/1_1... |
Es gibt im Datenbestand auch einige Bahnhöfe, die in der Schweiz liegen.
Beispielsweise Schaffhausen ist ein Gemeinschaftsbahnhof zwischen der Schweizerischen Bundesbahnen und dem deutschen Bundeseisenbahnvermögen. Quelle: Wikipedia
Auffällig ist, dass bei diesen Bahnhöfen keine Bilder und auch keine stationCategory / transportAssociations vorhanden sind, das scheint nur im Datenbestand der deutschen Bahnhöfe zu exisiteren.
df[df['countryCode']!='DE']
| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 347 | 424 | Basel | CH | 200 | 47.567288 | 7.607805 | {} | Basel Bad Bf | RB Südwest | DB S&S | 4016 | Schweiz CH | NaN | Schwarzwaldallee | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [RVL] | NaN |
| 2093 | 2698 | Schaffhausen | CH | 1 | 47.717003 | 8.664127 | {} | Herblingen | RB Südwest | DB S&S | 8207 | Schweiz CH | NaN | Bruderhalde | [CITY_TRAIN] | [] | NaN |
| 3368 | 4399 | Neuhausen | CH | 18 | 47.682615 | 8.612186 | {} | Neuhausen Bad Bf | RB Südwest | DB S&S | 8212 | Schweiz CH | NaN | Badischen Bahnhofstr. | [REGIONAL_TRAIN] | [] | NaN |
| 3387 | 4424 | Neunkirch | CH | 3 | 47.689151 | 8.495384 | {} | Neunkirch | RB Südwest | DB S&S | 8225 | Schweiz CH | NaN | Bahnhofstr. | [REGIONAL_TRAIN] | [] | NaN |
| 4053 | 5274 | Riehen | CH | 25 | 47.583157 | 7.652014 | {} | Riehen | RB Südwest | DB S&S | 4125 | Schweiz CH | NaN | Bahnhofstr. | [REGIONAL_TRAIN] | [RVL] | NaN |
| 4233 | 5530 | Schaffhausen | CH | 29 | NaN | NaN | {} | Schaffhausen | RB Südwest | DB S&S | 8200 | Schweiz CH | NaN | Bahnhofstr. | NaN | NaN | NaN |
| 4715 | 6192 | Thayngen | CH | 31 | 47.745502 | 8.704300 | {} | Thayngen | RB Südwest | DB S&S | 8240 | Schweiz CH | NaN | Bahnhofstr. | [CITY_TRAIN] | [] | NaN |
| 4743 | 6235 | Trasadingen | CH | 1 | 47.665238 | 8.436804 | {} | Trasadingen | RB Südwest | DB S&S | 8219 | Schweiz CH | NaN | Bahnhofstr. | [REGIONAL_TRAIN] | [] | NaN |
| 5128 | 6762 | Wilchingen | CH | 18 | 47.679448 | 8.463860 | {} | Wilchingen-Hallau | RB Südwest | DB S&S | 8217 | Schweiz CH | NaN | Bahnhofstrasse | [REGIONAL_TRAIN] | [] | NaN |
def plot_counts(column):
counts = df[column].value_counts().reset_index()
counts.columns = [column, 'count']
fig = px.bar(counts, x=column, y='count', barmode='group', text='count')
fig.show()
Die meisten Bahnhöfe gehören der DB Station&Service AG. Laut ihrer Webseite, unterhalten sie rund 5.400 Bahnhöfe.
Das können wir bestätigen! The DB S&S hat laut den Daten 5.413 Bahnhöfe. Der Rest, 277, werden von der DB Regio-Netze unterhalten.
plot_counts('owner')
Betrachtet man die Organisationsbereiche, gibt es viele Stationen in der mitte/süden/westen von Deutschland. Der Norden und Osten liegen hingegen auf den letzten Plätzen.
Es gibt auch einige kleinere Organisationseinheiten für spezielle Regionen.
plot_counts('organisationalUnit')
Tatsächlich sehen wir, dass die führenden Bundesländer Bayern, Baden-Württemberg und Nordrhein-Westfalen (NRW) sind.
Es besteht eine deutliche Lücke zwischen ihnen und dem viertplatzierten Bundesland Hessen. Natürlich müssen wir auch die Größe der Bundesländer berücksichtigen.
Wir können prüfen, welches Bundesland laut seiner Größe die meisten Bahnhöfe hat. Dazu rufen wir die Größe der Bundesländer ab und setzen sie ins Verhältnis mit der Anzahl der Stationen.
df_states = pd.read_csv(data_folder + 'states_size.csv', sep=';')
df_states['size'] = df_states['size'].astype(float)
df_states
| state | size | |
|---|---|---|
| 0 | Baden-Württemberg | 35747.82 |
| 1 | Bayern | 70541.57 |
| 2 | Berlin | 891.12 |
| 3 | Brandenburg | 29654.35 |
| 4 | Bremen | 419.62 |
| 5 | Hamburg | 755.09 |
| 6 | Hessen | 21115.64 |
| 7 | Mecklenburg-Vorpommern | 23295.45 |
| 8 | Niedersachsen | 47709.82 |
| 9 | Nordrhein-Westfalen | 34112.44 |
| 10 | Rheinland-Pfalz | 19858.00 |
| 11 | Saarland | 2571.11 |
| 12 | Sachsen | 18449.93 |
| 13 | Sachsen-Anhalt | 20459.12 |
| 14 | Schleswig-Holstein | 15804.30 |
| 15 | Thüringen | 16202.39 |
| 16 | Deutschland | 357587.77 |
df_grp_states = pd.DataFrame(df_stations.groupby(by='state').count()['id'].sort_values(ascending=False))
df_grp_states.rename(columns={'id': 'count'}, inplace=True)
df_grp_states
| count | |
|---|---|
| state | |
| Bayern | 1025 |
| Baden-Württemberg | 720 |
| Nordrhein-Westfalen | 711 |
| Hessen | 479 |
| Sachsen | 478 |
| Rheinland-Pfalz | 419 |
| Niedersachsen | 357 |
| Brandenburg | 310 |
| Sachsen-Anhalt | 289 |
| Thüringen | 289 |
| Mecklenburg-Vorpommern | 180 |
| Schleswig-Holstein | 137 |
| Berlin | 133 |
| Saarland | 77 |
| Hamburg | 58 |
| Bremen | 16 |
| Schweiz CH | 12 |
df_germany = pd.DataFrame(index=['count'], data={
'Deutschland':df_grp_states.sum().values[0]
}).T
df_grp_states = pd.concat([df_grp_states, df_germany])
df_grp_states['state'] = df_grp_states.index
df_grp_states = pd.merge(df_states, df_grp_states, how='left', on='state')
df_grp_states
| state | size | count | |
|---|---|---|---|
| 0 | Baden-Württemberg | 35747.82 | 720 |
| 1 | Bayern | 70541.57 | 1025 |
| 2 | Berlin | 891.12 | 133 |
| 3 | Brandenburg | 29654.35 | 310 |
| 4 | Bremen | 419.62 | 16 |
| 5 | Hamburg | 755.09 | 58 |
| 6 | Hessen | 21115.64 | 479 |
| 7 | Mecklenburg-Vorpommern | 23295.45 | 180 |
| 8 | Niedersachsen | 47709.82 | 357 |
| 9 | Nordrhein-Westfalen | 34112.44 | 711 |
| 10 | Rheinland-Pfalz | 19858.00 | 419 |
| 11 | Saarland | 2571.11 | 77 |
| 12 | Sachsen | 18449.93 | 478 |
| 13 | Sachsen-Anhalt | 20459.12 | 289 |
| 14 | Schleswig-Holstein | 15804.30 | 137 |
| 15 | Thüringen | 16202.39 | 289 |
| 16 | Deutschland | 357587.77 | 5690 |
df_grp_states['ratio'] = df_grp_states['size']/df_grp_states['count']
df_grp_states.sort_values('ratio', ascending=True).reset_index().drop(columns=['index'])
| state | size | count | ratio | |
|---|---|---|---|---|
| 0 | Berlin | 891.12 | 133 | 6.700150 |
| 1 | Hamburg | 755.09 | 58 | 13.018793 |
| 2 | Bremen | 419.62 | 16 | 26.226250 |
| 3 | Saarland | 2571.11 | 77 | 33.391039 |
| 4 | Sachsen | 18449.93 | 478 | 38.598180 |
| 5 | Hessen | 21115.64 | 479 | 44.082756 |
| 6 | Rheinland-Pfalz | 19858.00 | 419 | 47.393795 |
| 7 | Nordrhein-Westfalen | 34112.44 | 711 | 47.978115 |
| 8 | Baden-Württemberg | 35747.82 | 720 | 49.649750 |
| 9 | Thüringen | 16202.39 | 289 | 56.063633 |
| 10 | Deutschland | 357587.77 | 5690 | 62.844951 |
| 11 | Bayern | 70541.57 | 1025 | 68.821044 |
| 12 | Sachsen-Anhalt | 20459.12 | 289 | 70.792803 |
| 13 | Brandenburg | 29654.35 | 310 | 95.659194 |
| 14 | Schleswig-Holstein | 15804.30 | 137 | 115.359854 |
| 15 | Mecklenburg-Vorpommern | 23295.45 | 180 | 129.419167 |
| 16 | Niedersachsen | 47709.82 | 357 | 133.640952 |
Um das ganze auf einer Karte anzuzeigen, fehlen uns noch die Geo-Koordinaten der Bundesländer. Diese werden wie zuvor abgerufen.
geolocator = Nominatim(user_agent="my_app")
result={}
for entry in df_grp_states['state']:
result[entry] = geolocator.geocode(entry)
res_dict={}
# extract lat/lon
for index, entry in result.items():
if entry:
res_dict[index] = { 'latitude': entry[1][0], 'longitude': entry[1][1] }
geocode_result = pd.DataFrame().from_dict(res_dict).T
geocode_result['state'] = geocode_result.index
geocode_result
| latitude | longitude | state | |
|---|---|---|---|
| Baden-Württemberg | 48.537750 | 9.041169 | Baden-Württemberg |
| Bayern | 48.946756 | 11.403872 | Bayern |
| Berlin | 52.517037 | 13.388860 | Berlin |
| Brandenburg | 52.845549 | 13.246130 | Brandenburg |
| Bremen | 53.075820 | 8.807165 | Bremen |
| Hamburg | 53.550341 | 10.000654 | Hamburg |
| Hessen | 50.608065 | 9.028465 | Hessen |
| Mecklenburg-Vorpommern | 53.773506 | 12.575547 | Mecklenburg-Vorpommern |
| Niedersachsen | 52.839853 | 9.075962 | Niedersachsen |
| Nordrhein-Westfalen | 51.478921 | 7.554375 | Nordrhein-Westfalen |
| Rheinland-Pfalz | 49.953160 | 7.310646 | Rheinland-Pfalz |
| Saarland | 49.384187 | 6.953737 | Saarland |
| Sachsen | 50.929580 | 13.458505 | Sachsen |
| Sachsen-Anhalt | 52.008907 | 11.700334 | Sachsen-Anhalt |
| Schleswig-Holstein | 54.185400 | 9.822009 | Schleswig-Holstein |
| Thüringen | 50.901472 | 11.037784 | Thüringen |
| Deutschland | 51.163818 | 10.447831 | Deutschland |
df_grp_states_geo = pd.merge(df_grp_states, geocode_result, how='left', on='state')
df_grp_states_geo.drop(16, inplace=True) # drop germany
In diesem Fall nutzen wir plotly, um die Karte anzuzeigen.
Die Größe der Punkte zeigt die Anzahl der Haltestationen an.
Die Farbe gibt das Verhältnis zur Fläche an. Grün steht für deine hohe Dichte an Stationen, rot für eine niedrige.
Hier sehen wir nochmal, dass die Stadtstaaten eine hohe Dichte an Haltestationen aufweisen, allerdings absolut gesehen wenige Stationen haben (kleiner Kreis).
Die großen Bundesländer sind eher im mittleren Farbschema, wobei die im Norden rot gefärbt und damit Schlusslicht sind.
fig = px.scatter_geo(df_grp_states_geo,
lat='latitude',
lon='longitude',
hover_name='state', # Data to display when hovering over each data point
size='count', # Size of the markers
color='ratio', # Color of the markers
color_continuous_scale=['green','orange','red'],
projection='mercator',
scope='europe',
width=650,
height=800) # Map projection
fig.update_geos(center=dict(lon=10, lat=51), projection_scale=10)
fig.show()
Da die Verkehrsmittel und Verkehrsverbünde in geschachtelten Listen vorliegen, müssen diese zunächst geebnet werden, um die absolute Anzahl herauszufinden.
transports = []
for entry in df['transportAssociations']:
try:
for e in entry:
transports.append(e)
except:
pass
transportAssociations = pd.Series(transports).value_counts()
transports = []
for entry in df['availableTransports']:
try:
for e in entry:
transports.append(e)
except:
pass
availableTransports = pd.Series(transports).value_counts()
Spitzenreiter ist auch hier wieder Berlin, gefolgt von dem Rhein-Main-Verkehrsverbund (RMV).
Der RMV operiert in Hessen und ist der Nachfolger des Frankfurter Verkehrsverbundes (FVV) (Quelle: Wikipedia) Hier sieht man das Verkehrsgebiet in Hessen.
![]()
Die "NASA" ist der Nahverkehrsservice der Sachsen-Anhalt GmbH und hat lustigerweise auch die Webseite https://www.nasa.de/.
Man sieht auf dem Plan, dass hauptsächlich die Städt Leipzig, Halle, Dessau und Magdeburg verbunden werden und der "NASA" aus anderen Verkehrsverbünden bestehen, die ebenfalls hier auftauchen, wie der MDV und Übergänge zu anderen Verkehrsverbünden hat wie VMT, VRB und VBB.

Die in unserer Region bekannteren Verkehrsverbünde VVS und NALDO rangieren auf den mittleren Plätzen.
def plotBar(data, title, xlabel, ylabel, showlegend):
fig = px.bar(data, title=title)
fig.update_xaxes(title_text=xlabel)
fig.update_yaxes(title_text=ylabel)
fig.update_traces(showlegend=showlegend)
return fig
fig = plotBar(transportAssociations, 'Available Transport Associations', 'Transport Associations', 'Count', False)
fig.show()
Mit großem Abstand gibt es Haltestationen, an denen die REGIONAL_TRAIN hält. Leider ist nicht dokumentiert, was darunter zu verstehen ist.
Nach manueller Untersuchung der Daten werden damit sowohl Regionalbahnen (RB) wie auch S-Bahnen gemeint sein.
Allerdings werden die S-Bahn Stationen in der Stadt mit CITY_TRAIN markiert. Das ist also nicht ganz eindeutig.
Neben Zügen werden hier auch Busse (BUS) erfasst sowie CITY_TRAIN, was die Stadtbahnen/Tram/U-Bahn sowie S-Bahnen in der Stadt sind.
Im Vergleich dazu kommen die Schnellzüge INTERCITY_TRAIN (IC), HIGH_SPEED_TRAIN (ICE) und INTER_REGIONAL_TRAIN (IRE) fast schon selten vor. Das macht aber natürlich Sinn, weil diese nur an ausgewählten Bahnhöfen halten.
Hier gibt es eine Übersicht der Bahn über die Nah- und Fernverkehrszüge.
fig = plotBar(availableTransports, 'Available Transports', 'Transport Type', 'Count', False)
fig.show()
df_facilities.head(1)
| id | description | operatorname | state | stateExplanation | type | |
|---|---|---|---|---|---|---|
| 0 | 1 | zu Gleis 1 | DB Station&Service | ACTIVE | available | ELEVATOR |
Zunächst bringen wir die Daten in eine Form, die besser zu visualisieren ist.
Uns interessiert, welche Art (type) von facilities in welchem Zustand (state) ist.
Dazu wird anhand dieser beiden Werte gruppiert und die summierten Werte wieder in eine flache Struktur geformt.
Man könnte auch noch auf den operatorname eingehen, allerdings werden die allermeisten Einrichtungen wieder von der DB Station&Service betrieben.
df_facilities_grouped = df_facilities.groupby(['type', 'state']).count()
df_facilities_grouped = df_facilities_grouped.unstack()['id']
df_facilities_grouped
| state | ACTIVE | INACTIVE | UNKNOWN |
|---|---|---|---|
| type | |||
| ELEVATOR | 2392 | 126 | 51 |
| ESCALATOR | 820 | 140 | 21 |
df_facilities_grouped['Ratio'] = (df_facilities_grouped['INACTIVE']) / df_facilities_grouped['ACTIVE']
df_facilities_grouped
| state | ACTIVE | INACTIVE | UNKNOWN | Ratio |
|---|---|---|---|---|
| type | ||||
| ELEVATOR | 2392 | 126 | 51 | 0.052676 |
| ESCALATOR | 820 | 140 | 21 | 0.170732 |
Es gibt ein paar Daten, bei denen der Zustand unbekannt ist.
Darüber hinaus sind absolut und relativ gesehen aktuell mehr Rolltreppen kaputt als Aufzüge.
Relativ sind es mit über 15% (nach update eine Woche später: 17%) aktuell nicht funktionierende Rolltreppen wirklich viele.
fig = go.Figure()
for state in df_facilities_grouped.columns[:3]:
fig.add_trace(go.Bar(
x=df_facilities_grouped.index,
y=df_facilities_grouped[state],
name=state,
))
fig.update_layout(title='Zustand der Einrichtungen an Bahnhöfen',
xaxis_title='Typ',
yaxis_title='Anzahl',
barmode='group')
fig.show()
Wir können uns eine Karte anzeigen lassen, die alle Haltestationen mit zusätzliche Informationen anzeigt.
Ganz Deutschland anzuzeigen führt allerding zu Performanceproblemen, daher werden zunächst alle Marker ausgeblendet und können über den Filter je Bundesland hinzugeschaltet werden.
df.dropna(subset = ['latitude'], inplace=True)
Dazu wird zuerst pro Bundesland eine FeatureGroup erstellt, die initial ausgeblendet ist.
state_dict = {}
for i in df.index:
state_dict.setdefault(df['state'][i], folium.FeatureGroup(name=df['state'][i], show=False, autoZIndex=False))
state_dict
{'Nordrhein-Westfalen': <folium.map.FeatureGroup at 0x1b6a14e2610>,
'Baden-Württemberg': <folium.map.FeatureGroup at 0x1b69fda00a0>,
'Bayern': <folium.map.FeatureGroup at 0x1b6e12cb130>,
'Niedersachsen': <folium.map.FeatureGroup at 0x1b6a0460760>,
'Sachsen': <folium.map.FeatureGroup at 0x1b6a140c070>,
'Schleswig-Holstein': <folium.map.FeatureGroup at 0x1b6a140cca0>,
'Berlin': <folium.map.FeatureGroup at 0x1b6a140ca60>,
'Brandenburg': <folium.map.FeatureGroup at 0x1b6a140cbe0>,
'Rheinland-Pfalz': <folium.map.FeatureGroup at 0x1b6a140ceb0>,
'Hessen': <folium.map.FeatureGroup at 0x1b6a140cd30>,
'Hamburg': <folium.map.FeatureGroup at 0x1b6a140caf0>,
'Mecklenburg-Vorpommern': <folium.map.FeatureGroup at 0x1b6a140c820>,
'Thüringen': <folium.map.FeatureGroup at 0x1b6a140c520>,
'Sachsen-Anhalt': <folium.map.FeatureGroup at 0x1b6a140c970>,
'Saarland': <folium.map.FeatureGroup at 0x1b6a140c4c0>,
'Schweiz CH': <folium.map.FeatureGroup at 0x1b6a140c430>,
'Bremen': <folium.map.FeatureGroup at 0x1b6a140c400>}
Per HTML kann ein Popup definiert werden, das erscheint, wenn man auf den Pin klickt. Hierbei wird das Bild und die zugehörigen Verkehrsverbünde und Zugtypen angezeigt.
Auf dieser Karte kann man sehr schön erkennen, wo die Bahnlienien verlaufen und dass es gewisse Regionen gibt, die nicht an die Bahn angeschlossen sind.
Beim Klicken durch die Bilder fällt auch auf, dass die Bahnhöfe häufig ältere Gebäude sind, von den allerdings viele Renoviert wurden.
Die roten Marker identifizieren Bahnhöfe in denen ICEs halten, in den gelben "nur noch" die IREs.
def GetIcon(availableTransports):
try:
if ('HIGH_SPEED_TRAIN' in availableTransports):
return folium.Icon(color='red', icon='map-marker')
elif ('INTERCITY_TRAIN' in availableTransports):
return folium.Icon(color='orange', icon='map-marker')
except:
return folium.Icon(color='blue', icon='map-marker')
map_df = df
m = folium.Map(location=[50.111, 8.682],zoom_start=6) # limit with width=1500,height=1500 produces just white space around the map.
for i in map_df.index:
html=f"""
<img src="{map_df['image'][i]}" width="500px">
<br/>
<b><p>{map_df['id'][i]}: {map_df['name'][i]}</b></p>
<p>Transports: {map_df['availableTransports'][i]}</p>
<p>Associations: {map_df['transportAssociations'][i]}</p>
"""
parsedHtml = folium.Html(html, script=True)
popup = folium.Popup(parsedHtml, max_width=2650)
# this is probably done too often, but folium is smart enough
feature_group = state_dict[map_df['state'][i]]
m.add_child(feature_group)
folium.Marker(
location=[ map_df['latitude'][i], map_df['longitude'][i] ],
icon=GetIcon(map_df['availableTransports'][i]),
radius=8,
tooltip=map_df['name'][i],
popup=popup
).add_to(feature_group)
folium.LayerControl(collapsed=False).add_to(m)
m
Mögliche Werte von Type aus der Dokumentation:
df_local_services = loadData('local_services.pkl')
df_local_services.head()
| id | name | description | openingHours | latitude | longitude | type | |
|---|---|---|---|---|---|---|---|
| 0 | 1 | None | None | Mo-Su 06:15-22:30;PH 06:15-22:30 | MOBILE_TRAVEL_SERVICE | ||
| 1 | 1 | Duisburg Hbf | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER | ||
| 2 | 1 | None | None | None | RAILWAY_MISSION | ||
| 3 | 1 | None | Ja, um Voranmeldung unter 030 65 21 28 88 (Ort... | None | HANDICAPPED_TRAVELLER_SERVICE | ||
| 4 | 1 | None | None | None | LOCKER |
Man erkennt, dass die TRIPE_S_CENTER (Service, Sicerheit & Sauberkeit), TRAVEL_CENTER, TRAVEL_LOUNGE und VIDEO_TRAVEL_CENTER alle einen Namen, Öffnungszeiten und Geo-Koordinaten haben.
Die Services RAD_PLUS und TRAVEL_LOUNGE haben einen Namen, MOBILE_TRAVEL_SERVICE, INFORMATION_COUNTER und LOST_PROPERTY_OFFICE dafür Öffnungszeiten.
df_local_services.groupby(by='type').count().sort_values(by='id', ascending=False)
| id | name | description | openingHours | latitude | longitude | |
|---|---|---|---|---|---|---|
| type | ||||||
| TRIPLE_S_CENTER | 4103 | 4103 | 0 | 4103 | 4103 | 4103 |
| CAR_PARKING | 3136 | 0 | 0 | 0 | 3136 | 3136 |
| BICYCLE_PARKING | 3081 | 0 | 0 | 0 | 3081 | 3081 |
| TAXI_RANK | 1100 | 0 | 0 | 0 | 1100 | 1100 |
| PUBLIC_RESTROOM | 562 | 0 | 0 | 0 | 562 | 562 |
| TRAVEL_NECESSITIES | 508 | 0 | 0 | 0 | 508 | 508 |
| HANDICAPPED_TRAVELLER_SERVICE | 318 | 0 | 233 | 0 | 318 | 318 |
| TRAVEL_CENTER | 260 | 260 | 260 | 260 | 260 | 260 |
| RAD_PLUS | 260 | 260 | 0 | 0 | 260 | 260 |
| LOCKER | 168 | 0 | 0 | 0 | 168 | 168 |
| MOBILE_TRAVEL_SERVICE | 127 | 0 | 0 | 127 | 127 | 127 |
| WIFI | 119 | 0 | 0 | 0 | 119 | 119 |
| RAILWAY_MISSION | 89 | 0 | 0 | 0 | 89 | 89 |
| VIDEO_TRAVEL_CENTER | 87 | 87 | 87 | 87 | 87 | 87 |
| INFORMATION_COUNTER | 76 | 0 | 0 | 76 | 76 | 76 |
| LOST_PROPERTY_OFFICE | 70 | 0 | 70 | 70 | 70 | 70 |
| CAR_RENTAL | 67 | 0 | 0 | 0 | 67 | 67 |
| TRAVEL_LOUNGE | 14 | 14 | 0 | 14 | 14 | 14 |
def getDataByType(type):
filtered_ids = df_local_services[df_local_services['type'] == type]['id']
count = filtered_ids.count()
unique_count = filtered_ids.nunique()
print(type,"Count:", count)
print(type,"unique Count:", unique_count)
return df_local_services[df_local_services['type'] == type]
df_triples_center = getDataByType('TRIPLE_S_CENTER')
TRIPLE_S_CENTER Count: 4103 TRIPLE_S_CENTER unique Count: 4103
Wenn man sich die Daten anschaut, tauchen dort viele IDs doppelt auf und es scheint, als wären die Daten schwierig zu interpretieren.
Gruppiert man die Daten allerdings anhand des Typs, sind die IDs eindeutig und können somit gut analysiert werden.
df_triples_center.head(3)
| id | name | description | openingHours | latitude | longitude | type | |
|---|---|---|---|---|---|---|---|
| 1 | 1 | Duisburg Hbf | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER | ||
| 13 | 1000 | Dresden | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER | ||
| 16 | 1002 | Frankfurt (Main) Hbf | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER |
Die Öffnungszeiten sind in einem bestimmten Format angegeben, daher müssen diese erst geparst werden.
Zuvor wird aber geprüft, ob es überhaupt abweichungen gibt.
df_triples_center['openingHours'].unique()
array(['Mo-Su 00:00-24:00;PH 00:00-24:00'], dtype=object)
Scheinbar haben alle 3S-Center durchgehend geöffnet.
df_travel_center = getDataByType('TRAVEL_CENTER')
TRAVEL_CENTER Count: 260 TRAVEL_CENTER unique Count: 257
df_travel_center.head(1)
| id | name | description | openingHours | latitude | longitude | type | |
|---|---|---|---|---|---|---|---|
| 11 | 1 | DB Reisezentrum Aachen Hbf | Mo-Fr 06:00-21:00;Sa 07:00-20:00;Su 08:00-20:00 | 50.768944 | 6.0902 | TRAVEL_CENTER |
Bei den Reisezentren sehen die Öffnungszeiten schon interessanter aus.
df_travel_center['openingHours'].unique()
array(['Mo-Fr 06:00-21:00;Sa 07:00-20:00;Su 08:00-20:00',
'Mo-We 08:00-12:30,13:00-17:00;Th-Fr 08:00-12:30,13:00-18:00;Sa 08:00-13:30',
'Mo-Fr 08:00-17:00;Sa 08:00-13:00;Su 10:00-15:00',
'Mo 06:00-11:00,12:00-16:00;Tu-We,Fr 08:00-13:00,14:00-16:00;Th 09:00-13:00,14:00-19:00;Sa 08:00-12:00',
'Mo-Fr 06:30-12:00,13:00-18:30;Sa 08:00-13:00',
'Mo-Su 07:00-21:00',
'Mo-Fr 07:30-18:30;Sa-Su 09:00-13:00,13:30-17:00',
'Mo-Fr 09:00-12:00,13:00-17:25',
'Mo-Fr 08:00-12:00,13:00-18:00;Sa 08:30-13:30',
'Mo-Fr 08:00-18:00', 'Mo-Fr 07:00-19:00;Sa 09:00-14:00',
'Mo-Fr 07:00-11:30,12:00-14:30;Sa 08:30-13:30',
'Mo-Fr 06:30-09:00,09:30-17:30',
'Mo 06:30-18:30;Tu-Fr 07:30-18:30;Sa 06:30-11:30,12:00-14:30;Su 10:30-14:30,15:00-18:30',
'Mo-Fr 06:30-12:00,13:00-18:30', 'Mo-Fr 09:00-12:30,13:30-17:00',
'Mo 06:00-10:30,11:30-16:00;Tu 09:30-13:30,14:30-19:00;We-Fr 08:30-12:30,13:30-16:00;Sa 08:00-12:00',
'Mo-Fr 09:00-12:30,14:00-17:45',
'Mo 06:00-11:30,12:20-16:30;Tu-Fr 07:00-11:30,12:15-16:35',
'Mo-Fr 07:30-19:00;Sa-Su 09:00-18:00',
'Mo,Fr 06:15-16:35 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. ";Tu-Th 06:15-12:00,12:45-16:35 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. ";Sa 07:10-12:00 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. "',
'Mo-Fr 06:45-20:00;Sa-Su 07:00-20:00',
'Mo-Fr 08:00-18:00;Sa 09:00-16:00', 'Mo-Fr 06:30-11:45',
'Mo-Fr 07:00-18:00;Sa 08:00-12:00,12:30-14:30',
'Mo-Fr 07:00-21:00;Sa-Su 09:00-21:00',
'Mo 06:00-15:00,15:45-20:00;Tu-Fr 07:00-12:45,13:45-17:00;Sa-Su 09:30-15:00',
'Mo-We,Fr 08:30-13:00,13:45-18:00', 'Mo-Fr 06:30-17:30',
'Mo-Fr 05:45-18:45;Sa 08:00-13:00',
'Mo-Fr 07:30-12:30,13:30-17:30;Sa 07:30-12:30',
'Mo-Fr 08:00-13:00,14:00-17:45',
'Mo-Fr 07:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:00-13:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 08:30-18:30;Sa-Su 09:30-16:30',
'Mo 07:00-12:15,13:00-16:30;Tu-We,Fr 09:00-12:15,13:00-16:30;Th 09:00-12:15,13:00-18:00',
'Mo-Fr 07:30-19:00;Sa 08:15-15:00;Su 09:15-15:00',
'Mo-Fr 07:30-18:30;Sa 09:00-14:00',
'Mo 06:00-17:30;Tu-We,Fr 09:00-17:30;Th 09:00-20:00;Sa 08:00-16:30;Su 09:00-12:30,13:00-16:30',
'Mo-Fr 06:00-20:00;Sa 07:30-12:15,12:45-15:30',
'Mo-Fr 07:30-21:00;Sa-Su 08:30-18:30',
'Mo 06:30-17:50 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!";Tu,Fr 07:00-17:50 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!";We-Th 07:00-11:00,11:45-16:15 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!"',
'Mo-Fr 07:00-19:00;Sa 07:30-11:30,12:00-16:00;Su 09:30-15:00',
'Mo-Fr 06:00-20:30;Sa 07:00-14:30',
'Mo-Fr 07:30-19:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:30-15:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Su 11:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 07:00-11:30,13:30-18:00;Sa 09:00-14:00',
'Mo-Fr 07:00-20:00;Sa 08:30-19:00;Su 08:30-20:00',
'Mo-Fr 08:30-13:00,14:00-18:00;Sa 09:00-13:45',
'Mo 07:15-12:00,13:00-17:45;Tu-Fr 08:00-12:00,13:00-17:45;Sa 08:00-13:30',
'Mo-Fr 07:00-18:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum.";Sa 08:00-13:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum."',
'Mo-Fr 08:00-12:00,13:00-18:00;Sa 08:00-13:30',
'Mo-Fr 07:00-18:00;Sa 09:00-14:00',
'Mo-Fr 08:00-18:30;Sa 08:15-16:30;Su 12:00-17:00',
'Mo-Fr 07:00-19:00;Sa 09:00-14:30', 'Mo-Fr 07:15-17:15',
'Mo-Fr 08:30-17:30;Sa 08:30-13:30',
'Mo-Fr 07:45-12:00,13:30-18:00;Sa 08:10-13:45',
'Mo-Fr 09:00-18:00',
'Mo,Fr 07:00-18:00;Tu-Th 08:00-12:30,13:15-18:00;Sa 08:00-13:15;Su 10:00-15:15',
'Mo 06:00-15:00,15:30-20:00;Tu-Fr 07:00-12:30,13:30-17:00;Sa 07:00-12:30,13:00-15:00;Su 09:30-15:00',
'Mo-Fr 07:00-18:30;Sa 09:00-14:00',
'Mo-Fr 08:00-19:00;Sa 09:00-18:00;Su 10:00-16:00',
'Mo-Fr 07:45-12:30,13:30-17:45;Sa 09:00-14:00',
'Mo,Fr 06:00-18:00;Tu-Th 06:45-12:00,12:45-17:00;Su 09:00-12:30,13:00-17:00',
'Mo-Fr 07:00-19:00;Sa 09:00-17:00;Su 10:00-17:00',
'Mo-Fr 07:00-12:15,13:00-17:00;Sa 09:00-12:30,13:00-17:00;Su 08:00-12:30,13:00-15:00',
'Mo-Fr 08:00-12:30,13:30-17:00;Sa 10:00-15:30',
'Mo-Fr 07:15-12:15,13:00-17:30;Sa-Su 08:10-12:15,12:45-15:50',
'Mo-Fr 08:00-18:00 open "Wir bedienen Sie auch persönlich im DB Videoreisezentrum"',
'Mo-Fr 06:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00 - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr";Sa 07:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00 - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr";Su 08:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00 - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr"',
'Mo-Fr 06:00-21:00;Sa-Su 08:00-20:15', 'Mo-Fr 07:30-18:30',
'Mo-Fr 07:40-18:30;Sa 08:00-13:30;Su 09:00-14:30',
'Mo-Fr 06:30-19:00;Sa 08:00-17:30;Su 10:00-18:00',
'Mo-Fr 07:00-19:00;Sa 09:30-14:30',
'Mo-Sa 07:00-20:00;Su 08:00-20:00',
'Mo-Fr 09:00-12:00,13:00-17:15',
'Mo-Fr 07:45-13:00,13:45-17:30;Sa 07:45-12:45',
'Mo-Fr 07:00-20:00;Sa-Su 09:00-19:00',
'Mo-Fr 07:45-12:30,13:30-18:00;Sa 08:45-14:20',
'Mo-Fr 07:30-18:30;Sa 08:30-15:00;Su 10:00-16:00',
'Mo 07:00-12:30,13:00-17:00;Tu,Fr 08:00-12:30,13:00-17:00;We-Th 08:00-12:30,13:00-18:00;Sa 07:30-12:30,13:00-15:00;Su 10:00-15:00',
'Mo,Th 06:30-18:00;Tu-We,Fr 06:30-12:00,13:00-17:00;Sa 06:30-12:00,13:00-15:00',
'Mo-Fr 06:50-18:00;Sa 08:30-13:00',
'Mo,Fr 06:45-18:00;Tu-Th 07:20-12:00,13:00-17:30',
'Mo-Th 08:00-12:00,12:45-16:30;Fr 08:00-13:30',
'Mo-We,Fr 07:30-11:30,12:30-17:30;Th 06:00-15:00,15:30-20:00;Sa 08:30-14:00;Su 10:00-15:00',
'Mo-Fr 09:00-13:00,14:00-17:30;Sa 09:00-13:00',
'Mo-Fr 07:45-12:00,12:45-17:30;Sa 08:00-13:00',
'Mo 06:00-20:00;Tu-Fr 07:00-20:00;Sa-Su 08:00-13:00,14:00-16:30',
'Mo-We,Fr 07:30-12:30,13:30-17:30;Th 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:30-12:45,13:45-18:00;Sa 08:30-13:00',
'Mo-Fr 08:00-12:00,13:00-16:30',
'Mo 06:00-15:00,15:30-20:00;Tu-Fr 07:30-12:30,13:30-17:30;Sa-Su 09:45-15:00',
'Mo-Fr 06:30-18:00;Sa 09:00-15:30;Su 11:00-17:00',
'Mo-Fr 08:00-13:00,13:30-17:00;Sa 08:00-13:00',
'Mo,Th-Fr 08:00-16:30;Tu-We 08:00-12:30,13:00-17:00;Sa 08:00-12:30,13:00-16:00',
'Mo,Th-Fr 07:15-11:45,12:00-17:00;Tu-We 07:15-11:45,12:30-17:00;Su 10:00-15:00',
'Mo 12:30-18:00,8:00-12:00;Tu-Fr 08:00-12:00,12:30-18:00;Sa 08:00-12:00',
'Mo-Fr 07:00-19:00;Sa-Su 08:00-18:00',
'Mo-Fr 07:00-12:00,14:00-18:00;Sa 09:00-14:00',
'Mo-Fr 06:30-21:00;Sa 08:00-19:00;Su 09:00-20:00',
'Mo-Fr 08:00-12:30,13:30-18:00;Sa 09:00-13:00',
'Mo,We-Fr 07:00-12:45,13:45-17:00;Tu 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:00-12:00,12:45-17:00',
'Mo-Fr 07:00-19:30;Sa-Su 09:15-17:45',
'Mo-Fr 08:00-17:00;Sa 08:00-13:00',
'Mo-Fr 08:00-12:00,13:00-16:00',
'Mo-Fr 08:00-18:30;Sa 08:30-16:30',
'Mo-Fr 07:00-18:00;Sa 08:00-13:00',
'Mo-Fr 05:45-20:30;Sa-Su 06:45-20:00',
'Mo-Fr 08:00-13:00,14:00-18:00;Sa 08:30-13:00',
'Mo 06:00-19:30;Tu-Fr 07:00-19:30;Sa 08:00-17:00;Su 09:00-17:30',
'Mo-Fr 06:00-21:00;Sa-Su 07:00-21:00',
'Mo 06:00-19:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Tu-Th 07:00-19:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Fr 07:00-20:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Sa 09:00-17:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Su 10:00-18:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 "',
'Mo 07:30-12:00,12:45-16:30;Tu-Fr 08:30-12:00,12:45-16:30',
'Mo-Fr 07:45-12:15,13:00-17:45',
'Mo-Fr 09:00-12:00,13:00-17:25 open "Bitte Beachten: ;Am Montag, 31.07.23 geschlossen "',
'Mo-Fr 06:50-12:30,13:15-17:05;Sa 07:30-11:00,11:30-15:30;Su 08:45-12:00,12:30-17:15',
'Mo-Fr 08:00-18:00;Sa 09:00-13:00',
'Mo-Fr 08:00-12:00,13:00-17:00',
'Mo-Fr 06:00-18:00;Sa 07:00-14:00;Su 08:30-16:00',
'Mo-Fr 08:00-12:00,12:45-17:45;Sa 08:00-13:00',
'Mo-Fr 08:45-12:00,12:45-17:00',
'Mo-Fr 08:00-20:00;Sa 10:00-20:00;Su 10:00-18:00',
'Mo-Fr 08:00-18:00 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum"',
'Mo 07:00-11:30,12:15-16:00;Tu-Fr 08:00-12:15,13:00-16:00',
'Mo-Fr 08:30-12:15,12:45-16:00',
'Mo-Fr 07:00-19:00;Sa 08:00-16:00;Su 10:00-19:00',
'Mo-Fr 07:00-12:30,13:30-18:00;Sa 09:00-14:00',
'Mo-Fr 07:30-18:15 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr";Sa 08:00-15:00 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr";Su 10:00-15:00 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr"',
'Mo-Fr 08:30-13:00,14:00-18:00;Sa 08:30-13:00',
'Mo-Fr 05:45-20:00;Sa 06:30-19:30;Su 08:30-19:30',
'Mo-Fr 07:30-18:30 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum";Sa 09:00-14:00 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum"',
'Mo-Fr 08:00-11:30,12:00-17:00;Sa-Su 08:00-11:30,12:00-15:30',
'Mo-Fr 07:30-18:30;Sa 07:30-11:30,12:00-15:30;Su 09:30-13:30,14:00-17:30',
'Mo-Fr 06:00-20:00;Sa 07:00-19:00;Su 08:00-20:00',
'Mo-Fr 06:00-21:15;Sa 07:00-19:00;Su 08:00-20:00',
'Mo-Fr 06:00-21:00;Sa-Su 08:00-20:30',
'Mo-Fr 08:00-19:00;Sa 08:00-16:00;Su 09:00-16:00',
'Mo-Fr 08:00-12:15,12:45-16:00',
'Mo-Fr 06:45-18:35;Sa 07:30-13:00',
'Mo 06:00-10:30,11:30-16:00;Tu,Th-Fr 08:30-12:30,13:30-16:00;We 09:30-12:30,13:30-19:00;Sa 08:00-12:00',
'Mo-Fr 08:00-12:00,12:30-18:00;Sa 08:00-12:00',
'24/7 closed "Dauerhaft geschlossen"', 'Mo-Fr 06:15-16:40',
'Mo-Fr 07:00-20:00;Sa-Su 09:00-18:00',
'Mo-Fr 06:50-20:00;Sa-Su 07:50-19:00',
'Mo-Fr 07:00-20:00;Sa 08:00-18:00;Su 08:00-13:00,14:00-18:00',
'Mo-Fr 07:00-20:00;Sa-Su 08:00-18:00',
'Mo-Fr 08:00-12:00,13:00-16:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum"',
'Mo-Fr 06:00-10:30,11:00-15:30,16:00-21:00;Sa-Su 06:45-15:30,16:00-19:45',
'Mo-We 08:00-12:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Th-Fr 13:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 07:00-17:00;Sa 08:00-13:00',
'Mo-Fr 09:00-13:15,13:45-17:00',
'Mo-Fr 07:00-18:30;Sa 08:00-12:00,12:30-16:00;Su 10:00-15:30',
'Mo,We-Fr 08:00-12:30,13:30-18:00;Tu 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:30-12:20,12:50-16:30',
'Mo-We,Fr 07:00-12:45,13:45-17:00;Th 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 06:15-12:00,12:45-16:00;Sa 07:45-12:45',
'Mo-Fr 08:30-13:00,14:00-18:30;Sa 08:30-13:30',
'Mo-Fr 08:30-12:30,14:00-17:00',
'Mo,Fr 06:30-19:15 open "gültig ab 01.01.22";Tu-Th 07:30-12:30,13:15-17:15 open "gültig ab 01.01.22";Su 10:45-14:00,14:30-19:00 open "gültig ab 01.01.22"',
'Mo-Fr 07:00-21:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 09:00-19:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Su 10:00-20:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 09:00-12:30,13:00-16:45', 'Mo-Fr 09:10-12:15,13:30-17:50',
'Mo-Fr 07:00-20:00;Sa 08:00-18:00;Su 09:00-20:00',
'Mo-Fr 09:00-12:00,13:00-17:00 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum"',
'Mo-Fr 09:00-12:30,13:00-18:00;Sa 09:00-13:00,13:30-16:00;Su 12:00-15:30,16:00-20:00',
'Mo-Fr 08:00-18:00;Sa 09:00-14:00',
'Mo-Fr 07:30-19:00;Sa 09:00-18:00;Su 10:00-18:30',
'Mo-Fr 06:30-18:00;Sa 08:00-17:00;Su 10:00-13:30',
'Mo-Fr 07:00-18:00;Sa-Su 10:00-15:15',
'Mo-Fr 08:30-12:00,12:45-16:15 open "Nutzen Sie auch das Video-Reisezentrum am Vorplatz"',
'Mo-Fr 07:00-19:00;Sa 08:00-17:00;Su 09:00-18:00',
'Mo-Fr 09:00-13:00,13:30-17:00;Sa 08:00-13:00',
'Mo-Fr 06:15-12:00,12:35-16:50;Sa 08:30-14:00;Su 09:30-13:30',
'Mo-Th 08:00-18:00;Fr 08:00-13:00,14:00-18:00',
'Mo-Fr 10:00-16:00',
'Mo-Fr 08:00-12:30,13:15-18:00;Sa-Su 08:45-11:45,12:15-16:15',
'Mo-Fr 06:45-18:45;Sa 07:45-15:30',
'Mo-Fr 07:30-18:00;Sa 09:00-14:00;Su 09:30-13:30,14:00-17:00',
'Mo-Fr 08:15-12:15,13:00-17:45;Sa 07:15-12:30,13:00-15:45',
'Mo-Fr 07:30-18:30;Sa 08:30-14:00;Su 11:30-16:30',
'Mo 07:45-12:45,13:45-17:00;Tu,Th-Fr 07:00-12:45,13:45-17:00;We 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 07:30-13:00,14:00-18:00;Sa-Su 08:15-13:00,13:30-16:00',
'Mo-Fr 06:30-19:00;Sa 08:30-14:05',
'Mo-Fr 08:30-13:00,14:00-18:30;Sa 08:30-12:30',
'Mo-Fr 06:30-18:30;Sa 07:30-12:30;Su 11:30-16:30',
'Mo-Fr 08:00-20:00;Sa-Su 09:00-19:00',
'Mo-Fr 08:00-20:00;Sa-Su 10:00-20:00',
'Mo-Fr 07:00-18:00;Sa 08:30-16:00;Su 09:30-17:00',
'Mo-Fr 06:00-20:00;Sa 08:00-17:00;Su 09:00-18:00',
'Mo-Fr 08:30-13:15,14:15-18:10;Sa 08:30-13:15',
'Mo-Th 07:15-12:00,12:30-15:45',
'Mo-Fr 06:00-21:15;Sa-Su 07:00-19:00',
'Mo-We,Fr 07:30-13:00,14:00-17:00;Th 06:00-15:00,15:30-20:00;Sa 07:30-11:00,11:30-15:00;Su 09:30-15:00',
'Mo-Fr 07:45-12:45,13:30-18:00;Sa-Su 08:30-12:30,13:00-15:15',
'Mo-Fr 08:00-18:00 open "Bitte beachten Sie unsere geänderten Öffnungszeit am ;21.07.23 von 08:00-12:30 und 13:00-16:00 Uhr; Wir bedienen Sie auch persönlich im DB Videoreisezentrum "',
'Mo-Fr 06:30-18:00 open "Wir danken für Ihr Verständnis";Sa 08:30-13:30 open "Wir danken für Ihr Verständnis"',
'Mo-Fr 08:30-11:30,12:30-16:55 open " Wir bedienen Sie auch persönlich im DB Videoreisezentrum ;am Bahnsteig 1. "',
'Mo-Fr 06:30-12:10,12:55-16:50;Sa 06:45-12:15',
'Mo-Fr 08:00-18:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:00-13:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 06:00-19:00;Sa 08:00-17:00;Su 09:00-18:00',
'Mo-Fr 07:00-19:00;Sa 08:30-17:45;Su 09:30-18:00',
'Mo-Fr 08:50-12:30,13:45-17:30',
'Mo-Tu,Th-Fr 07:30-11:50,12:40-15:40',
'Mo-Fr 07:00-19:00;Sa 08:00-16:00;Su 09:30-18:30',
'Mo-Fr 08:30-12:30,13:45-17:10',
'Mo-Fr 07:00-19:00;Sa 08:00-17:30;Su 10:00-15:30',
'Mo-Tu,Th-Fr 08:00-12:30,13:30-18:00;We 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:15-17:30;Sa 09:15-13:00',
'Mo-Fr 08:00-12:30,13:30-17:30;Sa 08:30-12:30',
'Mo 06:00-17:30;Tu-Fr 07:30-17:30;Sa 07:00-09:45,10:15-13:30;Su 10:00-17:30',
'Mo-Fr 06:15-20:15;Sa-Su 08:15-18:15',
'Mo-Fr 06:00-22:00;Sa-Su 07:00-22:00',
'Mo-Fr 08:00-18:00;Sa 09:00-18:00',
'Mo-Fr 07:00-18:30;Sa 08:30-14:00',
'Mo 08:15-12:30,13:00-16:15;Tu-Fr 08:00-12:30,13:00-15:30;Sa 08:15-13:15'],
dtype=object)
def getDays(daysList):
days_of_week = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
start_index = days_of_week.index(daysList[0])
end_index = days_of_week.index(daysList[-1])
if start_index <= end_index:
days_between = days_of_week[start_index:end_index+1]
else:
days_between = days_of_week[start_index:] + days_of_week[:end_index+1]
return days_between
def get_minute_intervals(interval, time_range):
start_str, end_str = time_range.split('-')
start_time = datetime.strptime(start_str, '%H:%M')
end_time = datetime.strptime(end_str, '%H:%M')
interval = timedelta(minutes=interval)
current_time = start_time
intervals = []
while current_time <= end_time:
intervals.append(current_time.strftime('%H:%M'))
current_time += interval
return intervals
Mein Ziel war es, einen DataFrame mit den Öffnungszeiten in Stunden pro Tag pro ID zu erstellen.
Aktuell beinhalten die Daten nur die Öffnungszeiten pro Tag, ohne die genauen Uhrzeiten. Ich müsste vermutlich eine doppelte x-Achse verwenden, um sowohl die Tag- als auch die Uhrzeiten anzuzeigen.
data_rows = []
for entry in df_travel_center['openingHours']:
day_time_pairs = entry.split(';')
row = {}
for pair in day_time_pairs:
try:
days, times = pair.split(' ')
days = days.split('-')
fixed_list = []
for item in days:
if (',' in item):
day_item = item.split(',')
fixed_list.append(day_item)
else:
fixed_list.append(item)
allDays = getDays(fixed_list)
#print(allDays)
for day in allDays:
row[day] = 1
except Exception as e:
#print(e)
#times = pair.split(' ')
#print(times)
row[day] = 0
data_rows.append(row)
df_opening_hours = pd.DataFrame(data_rows)
df_opening_hours
| Mo | Tu | We | Th | Fr | Sa | Su | |
|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 1 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| 2 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 3 | 0.0 | NaN | NaN | 1.0 | NaN | 1.0 | NaN |
| 4 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 255 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 256 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| 257 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| 258 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN | NaN |
| 259 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
260 rows × 7 columns
Man kann sehen, dass in der Tabelle des öfteren NaN auftaucht. Wenn die Daten nicht verarbeitet werden konnten, lag es daran, dass dort noch Kommentare in den Zeiten vorhanden waren, wie beispielsweise aufgrund von Krankheit geschlossen.
Daher gehen wir davon aus, dass NaN ebenfalls geschlossen bedeutet.
df_opening_hours.replace(np.nan, 0, inplace=True)
df_opening_hours
| Mo | Tu | We | Th | Fr | Sa | Su | |
|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 1 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| 2 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 3 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 1.0 | 0.0 |
| 4 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 255 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 256 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| 257 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| 258 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 | 0.0 |
| 259 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
260 rows × 7 columns
Generell sieht man, dass die meisten Reisezentren am Wochenende geschlossen haben, vor allem am Sonntag. Vereinzelt sind aber auch unter der Woche Reisezentren geschlossen. Donnerstags scheint der Tag zu sein, an dem die meisten geöffnet haben.
custom_color_scale = [
[0.0, 'rgb(255, 0, 0)'], # closed = red
[1.0, 'rgb(0, 255, 0)'] # open = green
]
fig = go.Figure(data=go.Heatmap(
z=[[col for col in row] for _, row in df_opening_hours.iterrows()],
x=df_opening_hours.columns,
y=df_opening_hours.index,
colorscale=custom_color_scale
))
fig.update_layout(
title='Öffnungszeiten Reisezentren'
)
fig.show()
Hier sieht man dies noch einmal deutlicher, allerdings sind die Werktage unter der Woche alle ziemlich gleich, sodass es sich auch um Ungenauigkeiten in den Daten handeln kann.
df_opening_hours_grouped = df_opening_hours.sum().sort_values()
px.bar(df_opening_hours_grouped)
Nun können wir noch die Informationen der bahnhofsnahmen Dienstleistungen unserer Karte hinzufügen.
Dazu müssen die Dataframes zunächst wieder zusammengebracht werden.
df_station_facilities_by_id = df_station_facilities.groupby('id')['type'].unique().reset_index()
df_station_facilities_by_id.head(1)
| id | type | |
|---|---|---|
| 0 | 1 | [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW... |
df_station_facilities_by_id['id'] = df_station_facilities_by_id['id'].astype(int)
# del map_df_extended
map_df_extended = pd.merge(left=map_df, right=df_station_facilities_by_id, on=['id'], how='left')
map_df_extended.head(1)
| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | type | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen | DE | 2a | 50.7678 | 6.091499 | {} | Aachen Hbf | RB West | DB S&S | 52064 | Nordrhein-Westfalen | CATEGORY_2 | Bahnhofstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AAV, VRS] | https://api.railway-stations.org/photos/de/1_1... | [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW... |
Da in einigen Reihen von type NaN-Werte vorhanden sind, müssen wir dies zunächst austauschen. Immer wenn ein Wert nicht vom Typ Numpy Array ist (also NaN), erstellen wir eine neue leere Liste, um keine Daten zu verlieren.
def replace_nan_with_empty_array(value):
if isinstance(value, np.ndarray):
return value
else:
return np.array([])
map_df_extended['type'] = map_df_extended['type'].apply(replace_nan_with_empty_array)
map_df_extended
Jetzt wird wieder für jeden Typ eine FeatureGroup für die Karte erstellt.
facilities_dict = {}
for i in map_df_extended_clean.index:
facilities_dict.setdefault(map_df_extended_clean['type'][i][0], folium.FeatureGroup(name=map_df_extended_clean['type'][i][0], show=False, autoZIndex=False)) # TODO: current cheat: we always take only first value
facilities_dict
{'MOBILE_TRAVEL_SERVICE': <folium.map.FeatureGroup at 0x1b6e13ec970>,
'CAR_PARKING': <folium.map.FeatureGroup at 0x1b68bda7eb0>,
'TRIPLE_S_CENTER': <folium.map.FeatureGroup at 0x1b68bda7fd0>,
'LOCKER': <folium.map.FeatureGroup at 0x1b6a8240a00>,
'BICYCLE_PARKING': <folium.map.FeatureGroup at 0x1b6a8240b80>,
'RAD_PLUS': <folium.map.FeatureGroup at 0x1b6a82471f0>,
'HANDICAPPED_TRAVELLER_SERVICE': <folium.map.FeatureGroup at 0x1b6e12cb4c0>,
'PUBLIC_RESTROOM': <folium.map.FeatureGroup at 0x1b6a14ede20>,
'TRAVEL_NECESSITIES': <folium.map.FeatureGroup at 0x1b6a04acf70>,
'RAILWAY_MISSION': <folium.map.FeatureGroup at 0x1b6a04ac430>,
'INFORMATION_COUNTER': <folium.map.FeatureGroup at 0x1b680405be0>,
'TRAVEL_CENTER': <folium.map.FeatureGroup at 0x1b6d886a430>,
'TAXI_RANK': <folium.map.FeatureGroup at 0x1b6a06a3040>,
'VIDEO_TRAVEL_CENTER': <folium.map.FeatureGroup at 0x1b6a06a3070>,
'CAR_RENTAL': <folium.map.FeatureGroup at 0x1b6a06a30a0>,
'WIFI': <folium.map.FeatureGroup at 0x1b6a06a30d0>}
Letztlich werden beide Feature Groups, die von den Bundesländern und die der Einrichtungen zusammen auf die Karte gebracht.
map_df = map_df_extended_clean
m = folium.Map(location=[50.111, 8.682],zoom_start=6) # limit with width=1500,height=1500 produces just white space around the map.
for i in map_df.index:
html=f"""
<img src="{map_df['image'][i]}" width="500px">
<br/>
<b><p>{map_df['id'][i]}: {map_df['name'][i]}</b></p>
<p>Transports: {map_df['availableTransports'][i]}</p>
<p>Associations: {map_df['transportAssociations'][i]}</p>
<p>Local services: {map_df['type'][i]}</p>
"""
parsedHtml = folium.Html(html, script=True)
popup = folium.Popup(parsedHtml, max_width=2650)
feature_group = state_dict[map_df['state'][i]]
m.add_child(feature_group)
feature_group = facilities_dict[map_df['type'][i][0]] # TODO: current cheat: we always take only first value
m.add_child(feature_group)
folium.Marker(
location=[ map_df['latitude'][i], map_df['longitude'][i] ],
icon=GetIcon(map_df['availableTransports'][i]),
radius=8,
tooltip=map_df['name'][i],
popup=popup
).add_to(feature_group)
folium.LayerControl(collapsed=False).add_to(m)
m